import org.jtikz.TikzGraphics2D;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.io.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.undo.*;

/** QuiverPanel is the content that lies within each tab in the frame of the main program
 *
 */
public class QuiverPanel extends JPanel {
    private JTabbedPane                 tabs;
    public QuiverDrawingPanel           activeDrawingPanel;
    private static final long           serialVersionUID = 1L;
    public final Configuration          config = Configuration.getDefaultConfiguration();
    public Quiver                       quiver;

    /* node following the mouse when clicking a node to add a new one */
    private Node                        indicatedNode;
    private Arrow                       indicatedArrow;
    private Node                        heldNode, selectedNode, hoveredNode;
    private Set<Node>                   selectedNodes = new HashSet<Node>();
    private boolean                     nodeMoved, multipleNodesMoved,  arrowMoved;
    private Arrow                       selectedArrow, heldArrow;
    private LabelInput                  labelInput;
    private double                      oldXCoordinate, oldYCoordinate;
    private UndoManager                 undoManager = new UndoManager();
    private ArrayList<Representation>   representations = new ArrayList<Representation>();
    private Representation              activeRepresentation = null;
    private Relations                   relations;
    private Field                       field = new Field();
    private Rectangle                   mouseSelectionRectangle;
    private Point                       mouseAnchorPoint;

    private MoveNode                    moveNode; // initiated when holding a node, added when releasing the node
    private CompoundEdit                deleteSelectedNodes;

    // START CONSTRUCTORS //
    public QuiverPanel(Quiver quiver) {
        setLayout(new GridBagLayout());

        this.quiver = quiver;

        relations = new Relations(quiver);

        GridBagConstraints c = new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
                GridBagConstraints.BOTH, new Insets(0, 0, 0, 0),  0, 0);
        tabs = createTabbedPane();
        add(tabs, c);

        undoManager.setLimit(config.undoSize);
        updateUndoRedoActions();
    }
    // END CONSTRUCTORS //



    // START SETUP COMPONENTS //
    public JTabbedPane createTabbedPane() {
        final JTabbedPane tabs = new JTabbedPane();
        tabs.setFocusTraversalKeysEnabled(false);
        InputMap im = tabs.getInputMap(JTabbedPane.WHEN_IN_FOCUSED_WINDOW);
        im.put(config.nextDrawingKey, "navigateRight");
        im.put(config.prevDrawingKey, "navigateLeft");

        activeDrawingPanel = new QuiverDrawingPanel();
        tabs.addTab("Quiver", activeDrawingPanel);

        tabs.addChangeListener(new ChangeListener() {
            
            @Override
            public void stateChanged(ChangeEvent changeEvent) {
                if (tabs.getSelectedIndex() == 0) {
                    setActiveRepresentation(null);
                } else {
                    setActiveRepresentation(representations.get(tabs.getSelectedIndex() - 1));

                }
                activeDrawingPanel = (QuiverDrawingPanel) tabs.getComponentAt(tabs.getSelectedIndex());
            }
        });
        
        return tabs;
    }
    // END SETUP COMPONENTS //



    // START UNDO/REDO EVENTS //
    public void undo() {
        undoManager.undo();
        updateUndoRedoActions();
        notifyRepresentations();
        activeDrawingPanel.repaint();
    }

    public void redo() {
        undoManager.redo();
        updateUndoRedoActions();
        notifyRepresentations();
        activeDrawingPanel.repaint();
    }

    public boolean canUndo() {
        return undoManager.canUndo();
    }

    public boolean canRedo() {
        return undoManager.canRedo();
    }

    public void updateUndoRedoActions() {
        Actions.undoAction.setEnabled(canUndo());
        Actions.redoAction.setEnabled(canRedo());
    }

    private void notifyRepresentations() {
        for (Representation representation : representations) {
            representation.quiverChanged();
        }
    }
    // END UNDO/REDO EVENTS //


    
    // START RANDOM STUFF // TODO: FIND BETTER NAME
    public void clear() {
        quiver.clear();
        selectedNode = null;
        selectedArrow = null;
        hoveredNode = null;
        heldNode = null;
        heldArrow = null;
        indicatedNode = null;
        indicatedArrow = null;
        updateUndoRedoActions();
        activeDrawingPanel.repaint();
    }

    private void addUndoableEdit(UndoableEdit edit) {
        undoManager.addEdit(edit);
        updateUndoRedoActions();
    }
    // END RANDOM STUFF //



    // START MOVING //
    public void moveAutoArrangeQuiverToHeldNode() {//Auto-arranges all nodes in quiver relative to held node
        Set<Node> nodes = quiver.getNodes();
        int numNodes = nodes.size();
        for (Node node1 : nodes) {
            if (node1 == heldNode) {
                continue;
            }
            double fx = 0, fy = 0;
            for (Node node2 : nodes) {
                if (node1 == node2) {
                    continue;
                }
                // should preferred distance increase with number of nodes?
                double force = -0.5
                        * config.hookesConstant
                        * (config.preferredNodeDistance + 5 * numNodes - node1
                        .distance(node2));
                double angle = node1.getAngle(node2);
                fx += force * Math.cos(angle);
                fy += force * Math.sin(angle);
            }
            Point2D point = node1.getPoint();
            node1.moveTo(point.getX() + fx, point.getY() + fy);
        }
        activeDrawingPanel.repaint();
    }

    //Moves only the currently held node
    private void moveHeldNode(MouseEvent e) {
        Node node = quiver.getNode(e.getPoint(), heldNode);
        if (node != null) {
            heldNode.moveTo(node.getPoint());
        } else if (e.isShiftDown()) {
            heldNode.snapToGrid(e.getX(), e.getY());
        } else {
            heldNode.moveTo(e.getX(), e.getY());
        }
        nodeMoved = true;
    }

    //Move the currently held Arrow
    private void moveHeldArrow(MouseEvent e) {
        heldArrow.move(e.getX() - oldXCoordinate, e.getY() - oldYCoordinate);
        storeMouseCoordinates(e);
        arrowMoved = true;
    }

    //Move all selected nodes relative to one held node in the selection
    private void moveSelectedNodes(MouseEvent e) {
        double dx = e.getX() - oldXCoordinate;
        double dy = e.getY() - oldYCoordinate;
        for (Node node : selectedNodes) {
            node.move(dx, dy);
        }
        storeMouseCoordinates(e);
        multipleNodesMoved = true;
    }
    // END MOVING //



    // START MOUSE EVENTS //
    //Holds coordinates of given mouse event
    private void storeMouseCoordinates(MouseEvent e) {
        oldXCoordinate = e.getX();
        oldYCoordinate = e.getY();
    }
    // END MOUSE EVENTS




    // START NODE ACTIONS //
    public void deleteNode(Node node, boolean deletingMultiple) {
        Set<Relation> deletees = getRelationsOnNamedDeletion(node);
        if (!deletees.isEmpty()) {
            if (showConfirmDeleteRelationDialog() == JOptionPane.NO_OPTION) {
                return;
            }
        }
        for (Relation relation : deletees) {
            removeRelation(relation);
        }
        QuiverFrame.getInstance().relationListModel.update();

        if (deletingMultiple) {
            deleteSelectedNodes.addEdit(new RemoveNode(node));
        } else {
            addUndoableEdit(new RemoveNode(node));
        }
        if (indicatedNode != null) {
            quiver.removeNode(indicatedNode);
        }
    }

    public void deleteSelectedNodes() {
        Set<Relation> deletees = new HashSet<Relation>();

        for (Node node : selectedNodes) {
            deletees.addAll(getRelationsOnNamedDeletion(node));
        }

        if (!deletees.isEmpty()) {
            if (showConfirmDeleteRelationDialog() == JOptionPane.NO_OPTION) {
                return;
            }
        }

        for (Relation relation : deletees) {
            removeRelation(relation);
        }
        QuiverFrame.getInstance().relationListModel.update();

        deleteSelectedNodes = new CompoundEdit();

        for (Node node : selectedNodes) {
            deleteNode(node, true);
        }

        deleteSelectedNodes.end();
        addUndoableEdit(deleteSelectedNodes);
        clearSelectedNodes();
    }

    public void clearSelectedNodes() { //Clears and unhighlights set of selected nodes
        for (Node n : selectedNodes) {
            n.setHighlighted(false);
        }
        selectedNodes.clear();
    }

    private void selectOrDeselectNode(Node node) { //Selects and highlights input node if not selected and vice versa
        if (node != null)  {
            if (selectedNodes.contains(node))  {
                node.setHighlighted(false);
                selectedNodes.remove(node);
            } else {
                node.setHighlighted(true);
                selectedNodes.add(node);
            }
        }
    }

    public void indicateNode(Node node) {
        if (node == indicatedNode) {
            return;
        }
        if (node != null) {
            indicatedNode = new Node(node.getPoint().getX(), node.getPoint()
                    .getY());
            indicatedNode.setIndicated(true);
            indicatedArrow = new Arrow(selectedNode, indicatedNode);
        } else if (selectedNode != null) {
            indicatedNode = null;
            indicatedArrow = null;
        }
    }

    public void selectNode(Node node) {
        if (node == selectedNode) {
            return;
        }
        if (node != null) {
            selectedNode = node;
            selectedNode.setSelected(true);
            activeDrawingPanel.repaint();
        } else if (selectedNode != null) {
            selectedNode.setSelected(false);
            selectedNode = null;
            activeDrawingPanel.repaint();
        }
    }

    /** This is a method that selects and highlights all the nodes that lies within the borders of a given rectangle
     * @param selectionBox The selectionbox is merely a rectangle drawn by the user to select nodes
     *
     * */
    private void selectNodes(Rectangle selectionBox) {
        for (Node node : quiver.getNodes()) {
            if (selectionBox.contains(node.getPoint())) {
                selectedNodes.add(node);
                node.setHighlighted(true);
            } else {
                selectedNodes.remove(node);
                node.setHighlighted(false);
            }
        }
    }

    public void holdNode(Node node) {
        if (node == heldNode) {
            return;
        }
        if (node != null) {
            heldNode = node;
            heldNode.setHeld(true);
            activeDrawingPanel.repaint();
        } else if (heldNode != null) {
            heldNode.setHeld(false);
            heldNode = null;
            nodeMoved = false;
            activeDrawingPanel.repaint();
        }
    }

    public void hoverNode(Node node) {
        if (node == hoveredNode) {
            return;
        }
        if (node != null) {
            hoveredNode = node;
            hoveredNode.setHovered(true);
            activeDrawingPanel.repaint();
        } else if (hoveredNode != null) {
            hoveredNode.setHovered(false);
            hoveredNode = null;
            activeDrawingPanel.repaint();
        }
    }
    // END NODE ACTIONS //



    // START ARROW ACTIONS //
    public void selectArrow(Arrow arrow) {
        if (arrow == selectedArrow) {
            return;
        }
        if (selectedArrow != null) {
            selectedArrow.setSelected(false);
            selectedArrow = null;
            activeDrawingPanel.repaint();
        }
        if (arrow != null) {
            selectedArrow = arrow;
            selectedArrow.setSelected(true);
            activeDrawingPanel.repaint();
        }
    }

    public void holdArrow(Arrow arrow) {
        if (arrow == heldArrow) {
            return;
        }
        if (arrow != null) {
            heldArrow = arrow;
            heldArrow.setHeld(true);
            activeDrawingPanel.repaint();
        } else if (heldArrow != null) {
            heldArrow.setHeld(false);
            heldArrow = null;
            arrowMoved = false;
            activeDrawingPanel.repaint();
        }
    }
    // END ARROW ACTIONS //



    // START LABEL ACTIONS //
    public void selectLabel(Named object) {
        if (labelInput != null){
            labelInput.write();
            activeDrawingPanel.remove(labelInput);
        }
        if (object == null) {
            labelInput = null;
            return;
        }
        if (object instanceof Arrow && activeRepresentation != null) {
            labelInput = new MatrixInput(activeRepresentation, (Arrow) object);
        } else if (object instanceof Node && activeRepresentation != null) {
            labelInput = new DimensionInput(activeRepresentation, (Node) object);
        } else {
            labelInput = new NameInput(object);
        }
        activeDrawingPanel.add(labelInput);
    }
    // END LABEL ACTIONS //



    // START FIELD //
    public void setFiniteField() {
        int[] values = showFiniteFieldInputDialogs();
        field.setField("GF(" + values[0] + "," + values[1] + ")");
    }

    public void resetFieldToRationals() {
        field = new Field();
    }
    public void setField(Field f) {
        this.field = f;
    }
    public Field getField () {
        return field;
    }
    // END FIELD //



    // START RELATIONS //
    public boolean addRelation(String relationString) {
        Relation relation = quiver.createRelation(relationString);
        if (relation != null && !relations.contains(relation)) {
            relations.add(relation);
            QuiverFrame.getInstance().relationListModel.update();
            return true;
        }
        return false;
    }
    
    public void setRelations(Relations rel) {
        this.relations = rel;
    }

    public void removeRelation(Relation relation) {
        relations.remove(relation);
        QuiverFrame.getInstance().relationListModel.update();
    }

    public Relations getRelations() {
        return relations;
    }

    private Set<Relation> getRelationsOnNamedDeletion(Named element) {
        Set<Relation> deletees = new HashSet<Relation>();
        for (Relation relation : relations.getRelations()) {
            if (relation.contains(element)) {
                deletees.add(relation);
            }
        }
        return deletees;
    }
    // END RELATIONS //



    // START REPRESENTATIONS //
    public void addRepresentation() {
        addRepresentation("");
    }

    public void addRepresentation(String name) {
        addRepresentation(new Representation(quiver));
    }

    public void addRepresentation(Representation representation) {
        representations.add(representation);
        activeRepresentation = representation;
        activeDrawingPanel = new QuiverDrawingPanel();
        tabs.addTab(representation.getName(), activeDrawingPanel);
        tabs.setSelectedIndex(tabs.getTabCount() - 1);
    }

    public Representation getActiveRepresentation() {
        return activeRepresentation;
    }

    public void setActiveRepresentation(Representation repr) {
        activeRepresentation = repr;
        config.representationMode = (repr != null);
    }

    public ArrayList<Representation> getRepresentations() {
        return representations;
    }
    // END REPRESENTATIONS //



    // START DIALOGS //
    /**
     * displays a confirm dialog asking the user to confirm deletion of affected relations
     * @return JOptionPane.YES_OPTION or JOptionPane.NO_OPTION
     */
    private int showConfirmDeleteRelationDialog() {
        return JOptionPane.showConfirmDialog(QuiverFrame.getInstance(), "You are about to delete an\nelement" +
                " that exists in a relation.\nContinue?", "Deleting part of a relation",
                JOptionPane.YES_NO_OPTION);
    }

    /**
     * displays a series of JOptionPane's to capture the base and exponent for a Galois Field
     * @return an int array with two elements, base at [0] and exponent at [1]
     */
    private int[] showFiniteFieldInputDialogs() {
        int[] values = {-1, -1};

        while (values[0] == -1) {
            try {
                values[0] = Integer.parseInt(JOptionPane.showInputDialog(QuiverFrame.getInstance(),
                        "Enter base value(must be prime):",
                        "Base value input", JOptionPane.PLAIN_MESSAGE));
            } catch(NumberFormatException e) {
                JOptionPane.showMessageDialog(QuiverFrame.getInstance(), "Error! Could not convert to an integer",
                        "Try again", JOptionPane.ERROR_MESSAGE);
            }
            if (values[0] <= 0)
            {
                JOptionPane.showMessageDialog(QuiverFrame.getInstance(), "Error! Value less than or equal to zero",
                        "Try again", JOptionPane.ERROR_MESSAGE);
                values[0] = -1;
            }
        }

        while (values[1] == -1) {
            try {
                values[1] = Integer.parseInt(JOptionPane.showInputDialog(QuiverFrame.getInstance(),
                        "Enter exponent value:",
                        "Exponent value input", JOptionPane.PLAIN_MESSAGE));
            } catch(NumberFormatException e) {
                JOptionPane.showMessageDialog(QuiverFrame.getInstance(), "Error! Could not convert to an integer",
                        "Try again", JOptionPane.ERROR_MESSAGE);
            }
            if (values[1] <= 0)
            {
                JOptionPane.showMessageDialog(QuiverFrame.getInstance(), "Error! Value less than or equal to zero",
                        "Try again", JOptionPane.ERROR_MESSAGE);
                values[1] = -1;
            }
        }
        return values;
    }
    // END DIALOGS //



    // START UNDO/REDO INNER CLASSES //
    class AddNode extends AbstractUndoableEdit {
        private Node node;

        public AddNode(double x, double y) {
            this.node = quiver.addNode(x, y);
            notifyRepresentations();
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            quiver.removeNode(node);
            notifyRepresentations();
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            quiver.addNode(node);
            notifyRepresentations();
        }

        public Node getNode() {
            return node;
        }
    }

    class RemoveNode extends AbstractUndoableEdit {

        private Node node;

        public RemoveNode(Node node) {
            quiver.removeNode(node);
            this.node = node;
            notifyRepresentations();
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            quiver.addNode(node);
            notifyRepresentations();
        }
        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            quiver.removeNode(node);
            notifyRepresentations();
        }

    }

    class AddArrow extends AbstractUndoableEdit {
        private Arrow arrow;
        
        public AddArrow(Node start, Node end) {
            arrow = quiver.addArrow(start, end);
            notifyRepresentations();
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            quiver.removeArrow(arrow);
            notifyRepresentations();
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            quiver.addArrow(arrow);
            notifyRepresentations();
        }
    }

    class RemoveArrow extends AbstractUndoableEdit {
        private Arrow arrow;
        
        public RemoveArrow(Arrow arrow) {
            this.arrow = arrow;
            quiver.removeArrow(arrow);
            notifyRepresentations();
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            quiver.addArrow(arrow);
            notifyRepresentations();
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            quiver.removeArrow(arrow);
            notifyRepresentations();
        }
    }

    class MoveNode extends AbstractUndoableEdit {
        private Node node;
        private Point2D startPoint, endPoint;

        public MoveNode(Node node, Point2D startPoint) {
            this.node = node;
            this.startPoint = startPoint;
        }
        
        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            node.moveTo(startPoint);
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            node.moveTo(endPoint);
        }

        public void setEndPoint(Point2D endPoint) {
            this.endPoint = endPoint;
        }
    }
    
    class FlipArrow extends AbstractUndoableEdit {
        private Arrow arrow;
        
        public FlipArrow(Arrow arrow) {
            this.arrow = arrow;
            arrow.flip();
            notifyRepresentations();
        }

        @Override
        public void undo() throws CannotUndoException {
            super.undo();
            arrow.flip();
        }

        @Override
        public void redo() throws CannotRedoException {
            super.redo();
            arrow.flip();
        }
    }
    // END UNDO/REDO INNER CLASSES //

    class QuiverDrawingPanel extends JPanel implements MouseListener, MouseMotionListener {
        public QuiverDrawingPanel() {
            setFocusable(true);
            addMouseListener(this);
            addMouseMotionListener(this);
        }

        // START PAINTING //
        @Override
        public void paint(Graphics g) { //Draws/refreshes all graphics
            setBackground(config.backgroundColor);
            Graphics2D g2 = (Graphics2D) g;
            super.paint(g2);
            paintMouseSelectionRectangle(g2);
            if (config.gridVisible) {
                paintGrid(g2);
            }
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);

            for (Node node : quiver.getNodes()) {
                if (activeRepresentation != null) {
                    node.paint(g2, activeRepresentation);
                } else {
                    node.paint(g2);
                }
            }
            for (Arrow arrow : quiver.getArrows()) {
                if (arrow.getFromNode() == arrow.getToNode()
                        || !arrow.getFromNode().isClose(arrow.getToNode())) {
                    if (activeRepresentation != null) {
                        arrow.paint(g2, activeRepresentation);
                    } else {
                        arrow.paint(g2);
                    }
                }
            }
            if (indicatedArrow != null && selectedNode != null
                    && !indicatedNode.isClose(selectedNode)) {
                indicatedArrow.paint(g2);
                indicatedNode.paint(g2);
            }
            if (labelInput != null){
                labelInput.repaint();
            }
        }

        private void paintGrid(Graphics2D g) {//Draws up the grid one draws on top of
            double width = getWidth();
            double height = getHeight();
            g.setPaint(config.gridColor);
            for (double x = 0; x < width; x += config.gridSize) {
                g.drawLine((int) x, 0, (int) x, (int) height);
            }
            for (double y = 0; y < height; y += config.gridSize) {
                g.drawLine(0, (int) y, (int) width, (int) y);
            }
        }

        private void paintMouseSelectionRectangle(Graphics2D g) {//Draws a rectangle from mouse points given by mouse pressed and released
            super.paintComponent(g);
            if (mouseSelectionRectangle != null){
                g.setColor(new Color(128, 128, 255));
                g.fill(mouseSelectionRectangle);
//            g.draw(mouseSelectionRectangle); // Don't think this is necessary
            }
        }
        // END PAINTING //

        // START MOUSE EVENTS //
        // START MOUSE LISTENER //
        @Override
        public void mouseClicked(MouseEvent e) {
            selectLabel(null);
            Node node = quiver.getNode(e.getPoint());
            Named label = quiver.getLabel(e.getPoint());
            Arrow arrow = quiver.getArrow(e.getPoint());
            final boolean isNodeAlreadySelected = selectedNode != null;
            final boolean isUnderPointerNode = node != null;
            final boolean isUnderPointerArrow = arrow != null;
            final boolean isUnderPointerLabel = label != null;

            switch (e.getButton()) {
                case MouseEvent.BUTTON1:// Left mouse button clicked
                    if (config.representationMode) { //Frame is in representationmode, no edit available but selecting a label
                        selectLabel(label);
                        break;
                    }

                    if (e.isShiftDown()) { // Shift down, only selecting nodes is applicable.
                        selectOrDeselectNode(node);
                        break;
                    }
                    if (!isNodeAlreadySelected) { //All options for when a node is NOT already selected before this particular click

                        if (isUnderPointerNode) { //Selects and indicates (used for drawing arrows) if mouse pointer has clicked existing node.
                            selectNode(node);
                            indicateNode(node);
                        } else if (isUnderPointerArrow && e.isControlDown()) { // reverse direction of arrow
                            addUndoableEdit(new FlipArrow(arrow));
                        } else if (isUnderPointerLabel) {
                            selectLabel(label);
                        } else if (isUnderPointerArrow) { //Seems unused, selected arrow is never used for anything
                            selectArrow(arrow);
                        } else {                               //The mouse pointer har clicked on empty space, node added in it's place.
                            addUndoableEdit(new AddNode(e.getX(), e.getY()));
                            selectArrow(null);
                        }
                    } else { //All options for when a node IS selected before this particular click
                        if (node != null) { //Arrow is drawn between previously selected node and currently selected node
                            undoManager.addEdit(new AddArrow(selectedNode, node));
                            updateUndoRedoActions();
//                            addEvent();
                        } else if (isUnderPointerLabel) {
                            selectLabel(label);
                        } else {//Arrow is drawn between previously selected node and a new node, new node added on current mouse pointer.
                            CompoundEdit edit = new CompoundEdit();
                            AddNode an = new AddNode(e.getX(), e.getY());
                            edit.addEdit(an);
                            edit.addEdit(new AddArrow(selectedNode, an.getNode()));
                            edit.end();
                            addUndoableEdit(edit);
                        }
                        selectNode(null); //No node selected after
                    }
                    break;

                case MouseEvent.BUTTON3: //Right mouse button clicked
                    if (heldNode != null) {
                        heldNode.cancelMove();
                        holdNode(null);
                    } else if (isUnderPointerNode) { //Deletes right clicked node
                        deleteNode(node, false);
                        break;
                    } else if (isUnderPointerArrow) {//Deletes right clicked arrow
                        addUndoableEdit(new RemoveArrow(arrow));
                    }
                    if (selectedArrow != null) {//Unselects selected arrow
                        selectArrow(null);
                    }
                    if (isNodeAlreadySelected) {//Unselects selected node
                        selectNode(null);
                    }
                    if (!selectedNodes.isEmpty()) {//If mouse right clicked and nodes are selected, unselects
                        clearSelectedNodes();
                    }
                    selectLabel(null);//Deselects label
            }
            repaint();
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            // TODO Auto-generated method stub

        }

        @Override
        public void mouseExited(MouseEvent e) {
            // TODO Auto-generated method stub

        }

        @Override
        public void mousePressed(MouseEvent e) {
            requestFocus();
            selectLabel(null);
            mouseAnchorPoint = e.getPoint();
            mouseSelectionRectangle = new Rectangle(mouseAnchorPoint);//Prepares for rectangle selection, coordinates given in mouseDragged and drawn in paintMouseSelectionRectangle
            storeMouseCoordinates(e);
            switch (e.getButton()) {
                case MouseEvent.BUTTON1:
                    if (heldNode == null) { //Holds node if not already held and mouse pointer rests over node
                        Node node = quiver.getNode(e.getPoint());
                        if (node != null) {
                            moveNode = new MoveNode(node, e.getPoint());
                            holdNode(node);
                        }
                    }
                    if (heldArrow == null && heldNode == null) {// //Holds arrow if not already held and mouse pointer rests over arrow
                        Arrow arrow = quiver.getArrow(e.getPoint());
                        if (arrow != null) {
                            holdArrow(arrow);
                        }
                    }
                    break;
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            switch (e.getButton()) {
                case MouseEvent.BUTTON1:
                    if (heldNode != null) {
                        if (nodeMoved) {
                            Node node = quiver.getNode(e.getPoint(), heldNode);
                            if (node != null) {
                                // TODO: Upon undo of remove, the arrows should be copied back
                                quiver.copyArrows(heldNode, node);
                                undoManager.addEdit(new RemoveNode(heldNode));
                            } else {
                                moveNode.setEndPoint(e.getPoint());
                                undoManager.addEdit(moveNode);
                                moveNode = null;
                            }
                            updateUndoRedoActions();
                        }
                        holdNode(null);
                    }
                    if (heldArrow != null) {
                        if (arrowMoved) {
                            updateUndoRedoActions();
                        }
                        holdArrow(null);
                    }
                    if (!selectedNodes.isEmpty()) {
                        if (multipleNodesMoved) {
                            updateUndoRedoActions();
                        }
                    }
                    break;
            }
            // Removes mouseSelectionRectangle when mouse is released
            mouseSelectionRectangle = null;
            repaint();
        }
        // END MOUSE LISTENER //

        // START MOUSE MOTION LISTENER //
        @Override
        //Controls possible methods fired when you drag the mouse pointer
        public void mouseDragged(MouseEvent e) {
            final boolean holdsNode = heldNode != null;
            final boolean holdsArrow = heldArrow != null;
            final boolean holdsRectangle = mouseSelectionRectangle != null;

            final boolean isMultipleNodesMove = holdsNode && !selectedNodes.isEmpty() && selectedNodes.contains(heldNode);
            final boolean isSingleNodeMove = holdsNode && (nodeMoved || !heldNode.isClose(e.getPoint()));
            final boolean isArrowMove = holdsArrow && (arrowMoved || !heldArrow.isClose(e.getPoint()));
            final boolean isAreaSelection = !holdsNode && !holdsArrow && holdsRectangle;

            if (isMultipleNodesMove) {
                moveSelectedNodes(e);
            } else if (isSingleNodeMove) {
                moveHeldNode(e);
            } else if (isArrowMove) {
                moveHeldArrow(e);
            } else if (isAreaSelection) {
                drawSelectionRectangle(e);
            }

            activeDrawingPanel.repaint();
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            Node node = quiver.getNode(e.getPoint());
            if (node != null) {
                hoverNode(node);
            } else if (hoveredNode != null) {
                hoverNode(null);
            }
            if (indicatedNode != null && indicatedArrow != null) {
                if (hoveredNode != null && indicatedNode.isClose(hoveredNode)) {
                    indicatedNode.moveTo(hoveredNode.getPoint());
                } else {
                    indicatedNode.moveTo(e.getPoint());
                }
                indicatedArrow.update();
                activeDrawingPanel.repaint();
            }
        }
        // END MOUSE MOTION LISTENER //

        //Draws rectangle and selects nodes within the rectangle
        private void drawSelectionRectangle(MouseEvent e) {
            mouseSelectionRectangle.setBounds(Math.min(mouseAnchorPoint.x, e.getX()), Math.min(mouseAnchorPoint.y, e.getY()),
                    Math.abs(e.getX() - mouseAnchorPoint.x), Math.abs(e.getY() - mouseAnchorPoint.y));
            selectNodes(mouseSelectionRectangle);
        }
        // END MOUSE EVENTS //
    }
}
